/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.modules.micronaut.db;

import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Rectangle;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.swing.JPanel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.spi.project.ui.templates.support.Templates;
import org.openide.WizardDescriptor;
import org.openide.util.ChangeSupport;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;

/**
 *
 * @author Dusan Balek
 */
public class ClassesSelectorPanel extends javax.swing.JPanel {

    static final String PROP_CLASSES = "wizard-classes"; //NOI18N
    static final String PROP_SELECTED_CLASSES = "wizard-selected-classes"; //NOI18N

    private final ChangeSupport changeSupport = new ChangeSupport(this);
    private final Set<String> available = new HashSet<>();
    private final Set<String> selected = new HashSet<>();
    private final String bundleKey;

    public ClassesSelectorPanel(String key) {
        bundleKey = key;
        initComponents();
        ListSelectionListener selectionListener = new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                updateButtons();
            }
        };
        availableList.getSelectionModel().addListSelectionListener(selectionListener);
        selectedList.getSelectionModel().addListSelectionListener(selectionListener);
    }

    public void addChangeListener(ChangeListener listener) {
        changeSupport.addChangeListener(listener);
    }

    public void initialize(Set<String> entities) {
        available.addAll(entities.stream().map(fqn -> {
            int idx = fqn.lastIndexOf('.');
            return idx < 0 ? fqn : fqn.substring(idx + 1) + " (" + fqn.substring(0, idx) + ')';
        }).collect(Collectors.toSet()));
        String[] availableArray = available.toArray(new String[0]);
        Arrays.sort(availableArray);
        availableList.setListData(availableArray);
        selectedList.setListData(selected.toArray(new String[0]));
        updateButtons();
    }

    public Set<String> getSelectedClasses() {
        return selected.stream().map(item -> {
            int idx = item.indexOf(" (");
            return idx < 0 ? item : item.substring(idx + 2, item.length() - 1) + '.' + item.substring(0, idx);
        }).collect(Collectors.toSet());
    }

    private void updateButtons() {
        addButton.setEnabled(availableList.getSelectedIndices().length > 0);
        addAllButton.setEnabled(!available.isEmpty());
        removeButton.setEnabled(selectedList.getSelectedIndices().length > 0);
        removeAllButton.setEnabled(!selected.isEmpty());
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {
        java.awt.GridBagConstraints gridBagConstraints;

        classesPanel = new ClassesPanel();
        availableLabel = new javax.swing.JLabel();
        availableScrollPane = new javax.swing.JScrollPane();
        availableList = new javax.swing.JList<>();
        selectedLabel = new javax.swing.JLabel();
        selectedScrollPane = new javax.swing.JScrollPane();
        selectedList = new javax.swing.JList<>();
        buttonPanel = new javax.swing.JPanel();
        addButton = new javax.swing.JButton();
        removeButton = new javax.swing.JButton();
        addAllButton = new javax.swing.JButton();
        removeAllButton = new javax.swing.JButton();

        setMinimumSize(new java.awt.Dimension(200, 300));
        setName(org.openide.util.NbBundle.getMessage(ClassesSelectorPanel.class, "LBL_" + bundleKey));
        setPreferredSize(new java.awt.Dimension(496, 350));
        setLayout(new java.awt.GridBagLayout());

        classesPanel.setLayout(new java.awt.GridBagLayout());

        availableLabel.setLabelFor(availableList);
        org.openide.awt.Mnemonics.setLocalizedText(availableLabel, org.openide.util.NbBundle.getMessage(ClassesSelectorPanel.class, "LBL_Available" + bundleKey));
        availableLabel.setToolTipText(org.openide.util.NbBundle.getMessage(ClassesSelectorPanel.class, "LBL_Available" + bundleKey));
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
        gridBagConstraints.insets = new java.awt.Insets(0, 0, 5, 0);
        classesPanel.add(availableLabel, gridBagConstraints);

        availableScrollPane.setPreferredSize(new java.awt.Dimension(160, 130));

        availableList.setNextFocusableComponent(addButton);
        availableScrollPane.setViewportView(availableList);
        availableList.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(ClassesSelectorPanel.class, "ACSN_Available" + bundleKey));
        availableList.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(ClassesSelectorPanel.class, "ACSD_Available" + bundleKey));

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.weighty = 1.0;
        classesPanel.add(availableScrollPane, gridBagConstraints);

        selectedLabel.setLabelFor(selectedList);
        org.openide.awt.Mnemonics.setLocalizedText(selectedLabel, org.openide.util.NbBundle.getMessage(ClassesSelectorPanel.class, "LBL_Selected" + bundleKey));
        selectedLabel.setToolTipText(org.openide.util.NbBundle.getMessage(ClassesSelectorPanel.class, "LBL_Selected" + bundleKey));
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 2;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
        gridBagConstraints.insets = new java.awt.Insets(0, 0, 5, 0);
        classesPanel.add(selectedLabel, gridBagConstraints);

        selectedScrollPane.setPreferredSize(new java.awt.Dimension(160, 130));
        selectedScrollPane.setViewportView(selectedList);
        selectedList.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(ClassesSelectorPanel.class, "ACSN_Selected" + bundleKey));
        selectedList.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(ClassesSelectorPanel.class, "ACSD_Selected" + bundleKey));

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 2;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.weighty = 1.0;
        classesPanel.add(selectedScrollPane, gridBagConstraints);

        buttonPanel.setLayout(new java.awt.GridBagLayout());

        org.openide.awt.Mnemonics.setLocalizedText(addButton, org.openide.util.NbBundle.getMessage(ClassesSelectorPanel.class, "LBL_Add")); // NOI18N
        addButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                addButtonActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        buttonPanel.add(addButton, gridBagConstraints);

        org.openide.awt.Mnemonics.setLocalizedText(removeButton, org.openide.util.NbBundle.getMessage(ClassesSelectorPanel.class, "LBL_Remove")); // NOI18N
        removeButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                removeButtonActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.insets = new java.awt.Insets(5, 0, 0, 0);
        buttonPanel.add(removeButton, gridBagConstraints);

        org.openide.awt.Mnemonics.setLocalizedText(addAllButton, org.openide.util.NbBundle.getMessage(ClassesSelectorPanel.class, "LBL_AddAll")); // NOI18N
        addAllButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                addAllButtonActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 3;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.insets = new java.awt.Insets(17, 0, 0, 0);
        buttonPanel.add(addAllButton, gridBagConstraints);

        org.openide.awt.Mnemonics.setLocalizedText(removeAllButton, org.openide.util.NbBundle.getMessage(ClassesSelectorPanel.class, "LBL_RemoveAll")); // NOI18N
        removeAllButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                removeAllButtonActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 4;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.insets = new java.awt.Insets(5, 0, 0, 0);
        buttonPanel.add(removeAllButton, gridBagConstraints);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.gridheight = 2;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.insets = new java.awt.Insets(0, 11, 0, 11);
        classesPanel.add(buttonPanel, gridBagConstraints);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.ipady = 80;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.weighty = 1.0;
        gridBagConstraints.insets = new java.awt.Insets(6, 0, 0, 0);
        add(classesPanel, gridBagConstraints);
    }// </editor-fold>//GEN-END:initComponents

    private void removeAllButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeAllButtonActionPerformed
        available.addAll(selected);
        selected.clear();
        availableList.setListData(available.toArray(new String[0]));
        selectedList.setListData(selected.toArray(new String[0]));
        updateButtons();
        changeSupport.fireChange();
    }//GEN-LAST:event_removeAllButtonActionPerformed

    private void addAllButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addAllButtonActionPerformed
        selected.addAll(available);
        available.clear();
        availableList.setListData(available.toArray(new String[0]));
        selectedList.setListData(selected.toArray(new String[0]));
        updateButtons();
        changeSupport.fireChange();
    }//GEN-LAST:event_addAllButtonActionPerformed

    private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed
        available.addAll(selectedList.getSelectedValuesList());
        selected.removeAll(selectedList.getSelectedValuesList());
        availableList.setListData(available.toArray(new String[0]));
        selectedList.setListData(selected.toArray(new String[0]));
        updateButtons();
        changeSupport.fireChange();
    }//GEN-LAST:event_removeButtonActionPerformed

    private void addButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addButtonActionPerformed
        selected.addAll(availableList.getSelectedValuesList());
        available.removeAll(availableList.getSelectedValuesList());
        availableList.setListData(available.toArray(new String[0]));
        selectedList.setListData(selected.toArray(new String[0]));
        updateButtons();
        changeSupport.fireChange();
    }//GEN-LAST:event_addButtonActionPerformed


    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton addAllButton;
    private javax.swing.JButton addButton;
    private javax.swing.JLabel availableLabel;
    private javax.swing.JList<String> availableList;
    private javax.swing.JScrollPane availableScrollPane;
    private javax.swing.JPanel buttonPanel;
    private javax.swing.JPanel classesPanel;
    private javax.swing.JButton removeAllButton;
    private javax.swing.JButton removeButton;
    private javax.swing.JLabel selectedLabel;
    private javax.swing.JList<String> selectedList;
    private javax.swing.JScrollPane selectedScrollPane;
    // End of variables declaration//GEN-END:variables

    private final class ClassesPanel extends JPanel {

        @Override
        public void doLayout() {
            super.doLayout();

            Rectangle availableBounds = availableScrollPane.getBounds();
            Rectangle selectedBounds = selectedScrollPane.getBounds();

            if (Math.abs(availableBounds.width - selectedBounds.width) > 1) {
                GridBagConstraints buttonPanelConstraints = ((GridBagLayout)getLayout()).getConstraints(buttonPanel);
                int totalWidth = getWidth() - buttonPanel.getWidth() - buttonPanelConstraints.insets.left - buttonPanelConstraints.insets.right;
                int equalWidth = totalWidth / 2;
                int xOffset = equalWidth - availableBounds.width;

                availableBounds.width = equalWidth;
                availableScrollPane.setBounds(availableBounds);

                Rectangle buttonBounds = buttonPanel.getBounds();
                buttonBounds.x += xOffset;
                buttonPanel.setBounds(buttonBounds);

                Rectangle labelBounds = selectedLabel.getBounds();
                labelBounds.x += xOffset;
                selectedLabel.setBounds(labelBounds);

                selectedBounds.x += xOffset;
                selectedBounds.width = totalWidth - equalWidth;
                selectedScrollPane.setBounds(selectedBounds);
            }
        }
    }

    public static final class WizardPanel implements WizardDescriptor.FinishablePanel<WizardDescriptor>, ChangeListener {

        private final ChangeSupport changeSupport = new ChangeSupport(this);
        private final String title;
        private final String key;
        private final Function<Set<String>, String> validator;
        private ClassesSelectorPanel component;
        private boolean componentInitialized;
        private WizardDescriptor wizardDescriptor;
        private Map<String, Object> classes;

        public WizardPanel(String title, String key, Function<Set<String>, String> validator) {
            this.title = title;
            this.key = key;
            this.validator = validator;
        }

        @Override
        public ClassesSelectorPanel getComponent() {
            if (component == null) {
                component = new ClassesSelectorPanel(key);
                component.addChangeListener(this);
            }
            return component;
        }

        @Override
        public HelpCtx getHelp() {
                return new HelpCtx(ClassesSelectorPanel.class);
        }

        @Override
        public void addChangeListener(ChangeListener listener) {
            changeSupport.addChangeListener(listener);
        }

        @Override
        public void removeChangeListener(ChangeListener listener) {
            changeSupport.removeChangeListener(listener);
        }

        @Override
        public void readSettings(WizardDescriptor settings) {
            wizardDescriptor = settings;
            if (title != null) {
                wizardDescriptor.putProperty("NewFileWizard_Title", title); // NOI18N
            }
            if (!componentInitialized) {
                componentInitialized = true;
                classes = (Map<String, Object>) settings.getProperty(PROP_CLASSES);
                getComponent().initialize(classes.keySet());
            }
        }

        @Override
        public boolean isValid() {
            if (classes.isEmpty()) {
                setErrorMessage(NbBundle.getMessage(ClassesSelectorPanel.class, "ERR_No" + key, ProjectUtils.getInformation(Templates.getProject(wizardDescriptor)).getDisplayName()));
                return false;
            }
            String err = validator.apply(getComponent().getSelectedClasses());
            if (err != null) {
                setErrorMessage(err);
                return false;
            }
            setErrorMessage(" "); // NOI18N
            return true;
        }

        @Override
        public boolean isFinishPanel() {
            return !getComponent().getSelectedClasses().isEmpty();
        }

        @Override
        public void storeSettings(WizardDescriptor settings) {
            Set<String> selected = getComponent().getSelectedClasses();
            Map<String, Object> selEntities = classes.entrySet().stream().filter(entry -> selected.contains(entry.getKey())).collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue()));
            wizardDescriptor.putProperty(PROP_SELECTED_CLASSES, selEntities);
        }

        @Override
        public void stateChanged(ChangeEvent event) {
            changeSupport.fireChange();
        }

        private void setErrorMessage(String errorMessage) {
            wizardDescriptor.putProperty(WizardDescriptor.PROP_ERROR_MESSAGE, errorMessage);
        }
    }
}
